﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;

namespace TwoPageView3
{
    class TpvPdfExtractor
    {
        private static readonly int FILE_READ_FRAGMENT_SIZE = 1024 * 1024;

        public TpvPdfExtractor() {
        }

        private static int FindNextStr(BinaryReader reader, string[] findStr) {
            if (findStr.Length == 0) {
                return -1;
            }
            for (int i = 0; i < findStr.Length; ++i) {
                System.Diagnostics.Debug.Assert(findStr[i].Length != 0);
            }

            var nMatchArray = new int[findStr.Length];

            while (true) {
                char c;
                try {
                    c = (char)reader.ReadByte();
                } catch (EndOfStreamException) {
                    return -1;
                }

                for (int i = 0; i < findStr.Length; ++i) {
                    if (c == findStr[i][nMatchArray[i]]) {
                        ++nMatchArray[i];
                        if (findStr[i].Length <= nMatchArray[i]) {
                            return i;
                        }
                    } else {
                        if (' ' == c || '\t' == c) {
                            continue;
                        }
                        nMatchArray[i] = 0;
                    }
                }
            }
        }

        private static void FindNextStr(BinaryReader reader, string findStr) {
            int nMatch = 0;

            while (nMatch < findStr.Length) {
                char c = (char)reader.ReadByte();
                if (c == findStr[nMatch]) {
                    ++nMatch;
                } else {
                    if (' ' == c || '\t' == c) {
                        continue;
                    }
                    nMatch = 0;
                }
            }
        }

        private static List<byte> ReadBytesTillTheString(BinaryReader reader, string findStr) {
            var byteList = new List<byte>();
            int nMatch = 0;

            while (nMatch < findStr.Length) {
                byte b = reader.ReadByte();
                byteList.Add(b);
                char c = (char)b;
                if (c == findStr[nMatch]) {
                    ++nMatch;
                } else {
                    if (' ' == c || '\t' == c) {
                        continue;
                    }
                    nMatch = 0;
                }
            }

            if (nMatch == findStr.Length) {
                byteList.RemoveRange(byteList.Count - findStr.Length, findStr.Length);
            }

            return byteList;
        }

        private static void ConsumeCRLF(FileStream fs, BinaryReader reader) {
            while (true) {
                var b = reader.ReadByte();
                if (b != 0x0d && b != 0x0a) {
                    fs.Seek(-1, SeekOrigin.Current);
                    return;
                }
            }
        }

        private static void TrimCRLF(List<byte> byteList) {
            while (0 < byteList.Count) {
                var b = byteList[byteList.Count - 1];
                if (b == 0x0a || b == 0x0d) {
                    byteList.RemoveAt(byteList.Count - 1);
                } else {
                    return;
                }
            }
        }

        private static long FileCreateAndCopyStream(string newFileName, long fileSize, Stream s) {
            long pos = 0;
            using (FileStream fs = File.Create(newFileName)) {
                byte[] data = new byte[FILE_READ_FRAGMENT_SIZE];
                while (true) {
                    int size = s.Read(data, 0, data.Length);
                    if (size > 0) {
                        fs.Write(data, 0, size);
                        pos += size;
                    } else {
                        break;
                    }
                    if (fileSize < pos) {
                        System.Console.WriteLine("E: FileCreateAndCopyStream fileSize exceeded {0}", fileSize);
                        break;
                    }
                }
            }
            return pos;
        }

        private static long WriteToFile(byte[] content, string path) {
            using (FileStream fs = new FileStream(path, FileMode.Create)) {
                File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Temporary);
                using (BinaryWriter w = new BinaryWriter(fs)) {
                    w.Write(content);
                }
            }
            return content.Length;
        }

        /// <summary>
        /// read decimal number text from binary stream
        /// </summary>
        /// <returns>-1: fail</returns>
        private static int ReadDecimalText(BinaryReader reader) {
            System.Text.StringBuilder sb = new System.Text.StringBuilder();
            bool digitAppears = false;

            while (true) {
                char c = (char)reader.ReadByte();
                if (!digitAppears && System.Char.IsWhiteSpace(c)) {
                    continue;
                }
                if (!System.Char.IsDigit(c)) {
                    if (0 == sb.Length) {
                        return -1;
                    }
                    return System.Convert.ToInt32(sb.ToString(), System.Globalization.CultureInfo.InvariantCulture);
                }

                digitAppears = true;
                sb.Append(c);
            }
        }

        private static string ReadOneWord(BinaryReader reader) {
            System.Text.StringBuilder sb = new System.Text.StringBuilder();
            while (true) {
                char c = (char)reader.ReadByte();
                if (!System.Char.IsLetterOrDigit(c)) {
                    return sb.ToString();
                }
                sb.Append(c);
            }
        }

        public IEnumerable<TpvFileInfo> ExtractImages(string pdfFileName, string extractFolderName)
        {
            Console.WriteLine("D: PdfExtractToFolder({0}, {1})", pdfFileName, extractFolderName);
            bool done = false;
            int fileNumber = 0;

            System.DateTime lastWriteTime = File.GetLastWriteTimeUtc(pdfFileName);

            if (!Directory.Exists(extractFolderName)) {
                Directory.CreateDirectory(extractFolderName);
            }
            using (FileStream fs = new FileStream(pdfFileName, FileMode.Open, FileAccess.Read)) {
                using (BinaryReader r = new BinaryReader(fs)) {
                    while (!done) {
                        FindNextStr(r, new string[] { "/Subtype/Image", "/BitsPerComponent" });

                        var wh = new Size();
                        int imageSize = -1;
                        string filterName = string.Empty;

                        bool streamDone = false;
                        do {
                            var directive = FindNextStr(r, new string[] { "Filter/", "/Length ", "/Width", "/Height", "/Columns", "/Rows", "stream" });
                            switch (directive) {
                                case -1: // End of file
                                    streamDone = true;
                                    done = true;
                                    break;
                                case 0: // Filter/
                                    filterName = ReadOneWord(r);
                                    break;
                                case 1: // /Length
                                    imageSize = ReadDecimalText(r);
                                    break;
                                case 2: // /Width
                                    wh.Width = ReadDecimalText(r);
                                    break;
                                case 3: // /Height
                                    wh.Height = ReadDecimalText(r);
                                    break;
                                case 4: // /Columns
                                    wh.Width = ReadDecimalText(r);
                                    break;
                                case 5: // /Rows
                                    wh.Height = ReadDecimalText(r);
                                    break;
                                case 6: // stream
                                    string writePath;
                                    string fileName;
                                    long fileLength;

                                    byte[] imageData = null;
                                    if (0 < imageSize) {
                                        ConsumeCRLF(fs, r);
                                        imageData = r.ReadBytes(imageSize);
                                    } else {
                                        var byteList = new List<byte>();
                                        ConsumeCRLF(fs, r);
                                        byteList.AddRange(ReadBytesTillTheString(r, "endstream"));
                                        TrimCRLF(byteList);
                                        imageData = byteList.ToArray();
                                    }

                                    if ("CCITTFaxDecode".Equals(filterName)) {
                                        fileName = string.Format(CultureInfo.InvariantCulture, "{0:D5}.tif", fileNumber);
                                        fileLength = CCITTFaxToTIFF.CalcFileSizeFromImageBytes(imageData.Length);
                                    } else {
                                        fileName = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:D5}.jpg", fileNumber);
                                        fileLength = imageData.Length;
                                    }

                                    writePath = extractFolderName + Path.DirectorySeparatorChar + fileName;
                                    var existingFileInfo = new FileInfo(writePath);
                                    if (existingFileInfo.Exists && existingFileInfo.Length == fileLength) {
                                        // does not need to write
                                    } else {
                                        // write element to file
                                        if ("CCITTFaxDecode".Equals(filterName)) {
                                            imageData = CCITTFaxToTIFF.ConvertToTiff(imageData, wh);
                                        }
                                        WriteToFile(imageData, writePath);
                                        File.SetLastWriteTimeUtc(writePath, lastWriteTime);
                                    }

                                    string thumbnailCachePath = pdfFileName + Path.DirectorySeparatorChar + fileName;

                                    yield return new TpvFileInfo(writePath, thumbnailCachePath, fileNumber++, fileLength, lastWriteTime);
                                    streamDone = true;
                                    break;
                                default:
                                    System.Diagnostics.Debug.Assert(false);
                                    break;
                            }
                        } while (!streamDone);
                    }
                }
            }
        }
    }
}
